package proxy

import (
	"os"
	"sort"
	"strconv"

	"gitee.com/xuender/mysql-proxy/pb"
	"github.com/XiaoMi/soar/advisor"
	"github.com/XiaoMi/soar/common"
	"github.com/XiaoMi/soar/database"
	"github.com/xelabs/go-mysqlstack/sqlparser/depends/query"
	"github.com/xelabs/go-mysqlstack/sqlparser/depends/sqltypes"
	"github.com/xuender/oils/base"
	"github.com/xuender/oils/logs"
	"golang.org/x/exp/maps"
	"google.golang.org/protobuf/proto"
)

type CheckService struct {
	c          *pb.Config
	logSession base.Set[uint32]
	conns      map[uint32]*database.Connector
	logs       map[uint64]*pb.Log
}

func NewCheckService(config *pb.Config) *CheckService {
	conns := map[uint32]*database.Connector{}

	for _, conn := range config.Conns {
		conns[conn.Id] = base.Panic1(database.NewConnector(&common.Dsn{
			Net:                  "tcp",
			Schema:               conn.Schema,
			Timeout:              "3s",
			User:                 conn.Username,
			Password:             conn.Password,
			Addr:                 conn.Address,
			Charset:              conn.Charset,
			AllowNativePasswords: true,
		}))
	}

	return &CheckService{
		c:          config,
		conns:      conns,
		logSession: base.NewSet[uint32](),
		logs:       map[uint64]*pb.Log{},
	}
}

func (p *CheckService) Log(sql string, conID uint32) error {
	// logs.Infow("query", "id", session.ID(), "query", sql, "ip", session.Addr())
	finger := FingerID(sql)
	if log, has := p.logs[finger]; has {
		log.Count++

		p.SaveLog()
	} else {
		ret, err := p.Suggest(sql, conID)
		if err != nil {
			return err
		}

		if len(ret) > 0 {
			p.logs[finger] = pb.NewLog(finger, sql, ret)
			p.SaveLog()
		}
	}

	return nil
}

func (p *CheckService) ExplainAdvisor(sql string, connID uint32) map[string]advisor.Rule {
	conn, has := p.conns[connID]
	if !has {
		logs.Warnw("explatin conn not find", "id", connID)

		return nil
	}

	// logs.Debugw("conn", "version", base.Panic1(conn.Version()))

	explainInfo, err := conn.Explain(sql, 1, 0)
	if err != nil {
		logs.Errorw("explatin", "error", err)

		return nil
	}

	return advisor.ExplainAdvisor(explainInfo)
}

func (p *CheckService) HeuristicSuggest(sql string) (map[string]advisor.Rule, error) {
	audit, syntaxErr := advisor.NewQuery4Audit(sql)
	if syntaxErr != nil {
		return nil, syntaxErr
	}

	// 启发式建议
	heuristicSuggest := make(map[string]advisor.Rule)

	for item, rule := range advisor.HeuristicRules {
		okFunc := (*advisor.Query4Audit).RuleOK
		if &rule.Func != &okFunc {
			r := rule.Func(audit)
			if r.Item == item {
				heuristicSuggest[item] = r
			}
		}
	}

	return heuristicSuggest, nil
}

func (p *CheckService) Suggest(sql string, connID uint32) (map[string]advisor.Rule, error) {
	heuristicSuggest, err := p.HeuristicSuggest(sql)
	if err != nil {
		return nil, err
	}

	if rules := p.ExplainAdvisor(sql, connID); rules != nil {
		for key, value := range rules {
			heuristicSuggest[key] = value
		}
	}

	delete(heuristicSuggest, "OK")

	if rules := p.ExplainAdvisor(sql, connID); rules != nil {
		for key, value := range rules {
			heuristicSuggest[key] = value
		}
	}

	return heuristicSuggest, nil
}

func (p *CheckService) Check(sql string, id uint32) *sqltypes.Result {
	suggests, err := p.Suggest(sql, id)
	if err != nil {
		return Error(err)
	}

	logs.Debugw("check", "sql", sql)

	if len(suggests) > 0 {
		ret := &sqltypes.Result{
			Fields: []*query.Field{
				{Name: "代号", Type: query.Type_VARCHAR},
				{Name: "危险等级", Type: query.Type_VARCHAR},
				{Name: "摘要", Type: query.Type_VARCHAR},
				{Name: "解释", Type: query.Type_VARCHAR},
				{Name: "示例", Type: query.Type_VARCHAR},
				{Name: "位置", Type: query.Type_VARCHAR},
			},
			RowsAffected: uint64(len(suggests)),
			Rows:         [][]sqltypes.Value{},
		}

		for _, value := range suggests {
			values := []sqltypes.Value{
				base.Panic1(sqltypes.NewValue(query.Type_VARCHAR, []byte(value.Item))),
				base.Panic1(sqltypes.NewValue(query.Type_VARCHAR, []byte(value.Severity))),
				base.Panic1(sqltypes.NewValue(query.Type_VARCHAR, []byte(value.Summary))),
				base.Panic1(sqltypes.NewValue(query.Type_VARCHAR, []byte(value.Content))),
				base.Panic1(sqltypes.NewValue(query.Type_VARCHAR, []byte(value.Case))),
				base.Panic1(sqltypes.NewValue(query.Type_VARBINARY, []byte(strconv.Itoa(value.Position)))),
			}

			ret.Rows = append(ret.Rows, values)
		}

		return ret
	}

	return nil
}

func (p *CheckService) SaveLog() {
	values := maps.Values(p.logs)

	sort.Slice(values, func(i, j int) bool { return values[i].Count > values[j].Count })

	logs := &pb.Logs{
		Logs: values,
	}

	file := base.Panic1(os.Create("logs.pb"))
	defer file.Close()

	_, _ = file.Write(base.Panic1(proto.Marshal(logs)))
}

// nolint
func init() {
	common.Log.SetLevel(0)
}
